AnEx Learning Summit
Thursday October 21, 2021

Sarah Rankin
Quantitative Data Analyst
Stategy and Public Impact
New York Public Library
sarahrankin@nypl.org

Topics

Setup

This is an Rmarkdown notebook. If you’re viewing it in a web browser, you can click on Code->Download Rmd in the top right corner, then open it in RStudio and to run/edit the code. If you don’t use RStudio you can also just copy the code into your editor of choice.

We’ll be using a variety of tidyverse packages as well as sf and leaflet. If you don’t have these packages installed, uncomment the following code and install them now. (It’ll take a little while.)

# install.packages("tidyverse")
# install.packages("sf")
# install.packages("leaflet")
#load just the tidyverse for now
library(tidyverse)

Quick ggplot refresher

Let’s create some very simple point data.

five_points <- data.frame(var1 = 1:5, var2 = as.integer(c(3,5,4,4,1)))
five_points 
five_points %>% 
  ggplot(aes(x=var1, y=var2)) +
  geom_point()

five_points <- five_points %>%  
  mutate(var1_odd_or_even = ifelse(var1%%2==0,"even","odd"),
         var2_odd_or_even = ifelse(var2%%2==0,"even","odd"))

five_points
five_points %>% 
  ggplot(aes(x=var1, y=var2,color = var1_odd_or_even)) +
  geom_point(size = 5) 

Aesthetics in ggplot

  • Color is for lines (including outlines of shapes)

  • Fill is for areas (filling in shapes)

    • but our points are filled in based on the color aesthetic?!

    • the default point shape is shape 19 (“circle”) - just a solid circle with the color taken from the color aesthetic; shape 21 (“filled circle”) is a circle with a fill and an outline, which can be specified separately

  • Generally, you can either map the aesthetic to a data point or specify its value outside of the aes() function. If you do neither, a default will be used.

  • Aesthetic mapping/specification can be done in the original ggplot() call, or within individual geoms

Good general reference on specifying aesthetics:

https://ggplot2.tidyverse.org/articles/ggplot2-specs.html#point-1

five_points %>% 
  ggplot(aes(x=var1, 
             y=var2,
             color = var1_odd_or_even,
             fill = var2_odd_or_even)) +
  geom_point(size = 5, 
             shape = 21,
             stroke = 2) 

Points as Maps

  • Longitude and Latitude are (on a 2d map) x and y coordinates

  • Let’s get some coordinate data from the NYC facilities database

https://data.cityofnewyork.us/City-Government/Facilities-Database/ji82-xba5

public_libraries <- jsonlite::fromJSON("https://data.cityofnewyork.us/resource/ji82-xba5.json?factype=PUBLIC%20LIBRARY") 

public_libraries %>% str()
'data.frame':   221 obs. of  34 variables:
 $ uid       : chr  "015fec883329f3481f1711a39386a5ee" "05375983dd54899915412bc855795f75" "0616b9a31fe2db14793561377be01c0e" "08d71649b166891363a946ebb0b3b029" ...
 $ facname   : chr  "MORRIS PARK LIBRARY" "NORTH FOREST PARK" "58TH STREET LIBRARY" "FLUSHING" ...
 $ addressnum: chr  "985" "98-27" "127" "41-17" ...
 $ streetname: chr  "MORRIS PARK AVENUE" "METROPOLITAN AVENUE" "EAST 58 STREET" "MAIN STREET" ...
 $ address   : chr  "985 MORRIS PARK AVENUE" "98-27 METROPOLITAN AVENUE" "127 EAST 58 STREET" "41-17 MAIN STREET" ...
 $ city      : chr  "BRONX" "FOREST HILLS" "NEW YORK" "FLUSHING" ...
 $ boro      : chr  "BRONX" "QUEENS" "MANHATTAN" "QUEENS" ...
 $ borocode  : chr  "2" "4" "1" "4" ...
 $ zipcode   : chr  "10462" "11375" "10022" "11355" ...
 $ latitude  : chr  "40.84806105240" "40.71118635740" "40.76223213860" "40.75778232920" ...
 $ longitude : chr  "-73.85697658820" "-73.85367190670" "-73.96932374730" "-73.82887651450" ...
 $ xcoord    : chr  "1023819.55661000000" "1024817.14797000000" "992747.99653700000" "1031658.10613000000" ...
 $ ycoord    : chr  "248281.25916200000" "198414.74758100000" "216979.94069500000" "215403.54794900000" ...
 $ bin       : chr  "2045398" "4076687" "1037165" "4114282" ...
 $ bbl       : chr  "2041260040.00000000000" "4032070022.00000000000" "1013130005.00000000000" "4050430011.00000000000" ...
 $ commboard : chr  "211" "406" "105" "407" ...
 $ council   : chr  "13" "29" "4" "20" ...
 $ censtract : chr  "25200" "72900" "11203" "85300" ...
 $ nta       : chr  "BX37" "QN17" "MN19" "QN22" ...
 $ facgroup  : chr  "LIBRARIES" "LIBRARIES" "LIBRARIES" "LIBRARIES" ...
 $ facsubgrp : chr  "PUBLIC LIBRARIES" "PUBLIC LIBRARIES" "PUBLIC LIBRARIES" "PUBLIC LIBRARIES" ...
 $ factype   : chr  "PUBLIC LIBRARY" "PUBLIC LIBRARY" "PUBLIC LIBRARY" "PUBLIC LIBRARY" ...
 $ capacity  : chr  "0" "0" "0" "0" ...
 $ optype    : chr  "Public" "Public" "Public" "Public" ...
 $ opname    : chr  "New York Public Library" "Queens Public Library" "New York Public Library" "Queens Public Library" ...
 $ opabbrev  : chr  "NYPL" "Public" "NYPL" "Public" ...
 $ overlevel : chr  "City" "City" "City" "City" ...
 $ overagency: chr  "New York Public Library" "Queens Public Library" "New York Public Library" "Queens Public Library" ...
 $ overabbrev: chr  "NYPL" "QPL" "NYPL" "QPL" ...
 $ datasource: chr  "nypl_libraries" "qpl_libraries" "nypl_libraries" "qpl_libraries" ...
 $ facdomain : chr  "LIBRARIES AND CULTURAL PROGRAMS" "LIBRARIES AND CULTURAL PROGRAMS" "LIBRARIES AND CULTURAL PROGRAMS" "LIBRARIES AND CULTURAL PROGRAMS" ...
 $ schooldist: chr  "11" "28" "2" "25" ...
 $ policeprct: chr  "49" "112" "18" "109" ...
 $ servarea  : chr  "Local" "Local" "Local" "Local" ...

We need latitude and longitude to be numeric, not strings

public_libraries <- public_libraries %>%  
  mutate(latitude=as.numeric(latitude),longitude=as.numeric(longitude))

public_libraries %>% 
  select(facname,latitude,longitude) %>% 
  str()
'data.frame':   221 obs. of  3 variables:
 $ facname  : chr  "MORRIS PARK LIBRARY" "NORTH FOREST PARK" "58TH STREET LIBRARY" "FLUSHING" ...
 $ latitude : num  40.8 40.7 40.8 40.8 40.7 ...
 $ longitude: num  -73.9 -73.9 -74 -73.8 -73.7 ...
  • Map the longitude and latitude to the x and y aesthetics

    • Everyone has a strategy for remembering which is which - I like to think of soup dumplings - “Xiao long bao” - credit to @seankross
public_libraries %>% 
  ggplot(aes(x=longitude,
             y=latitude)) + 
  geom_point() 

  • That does not look like a map. Let’s look at our data.
public_libraries %>% 
  select(latitude,longitude) %>% 
  summary()
    latitude       longitude     
 Min.   : 0.00   Min.   :-74.24  
 1st Qu.:40.66   1st Qu.:-73.98  
 Median :40.72   Median :-73.92  
 Mean   :40.35   Mean   :-73.25  
 3rd Qu.:40.77   3rd Qu.:-73.85  
 Max.   :40.90   Max.   :  0.00  
  • At least some of our lat/long values are way out of expected range
public_libraries %>% 
  filter(latitude<39|longitude>74) %>% 
  select(latitude,longitude,facname,addressnum,streetname,address,city,boro)
  • Looks like we can clean this up by just filtering out these two cases
 public_libraries <- public_libraries %>%   
  filter(!latitude==0,
         !longitude==0)
 
 public_libraries %>% nrow()
[1] 219
  • Try again
public_libraries %>% 
  ggplot(aes(x=longitude,
             y=latitude)) + 
  geom_point() 

  • Make it a bit prettier

    • Color the points and rename the legend with scale_color_manual()

    • Get rid of axes and background with theme_void()

#define some system colors - a named vector of colors will map the colors to the values of the color aesthetic (backticks let you use non-syntactic R names)
#color values can be names from R's built-in colors or hex codes

library_system_colors <- c(`New York Public Library`="red3",
                   `Brooklyn Public Library` = "orange2",
                   `Queens Public Library` = "purple3")

public_libraries %>% 
  ggplot(aes(x=longitude,
             y=latitude,
             color = overagency)) + 
  geom_point() +
  scale_color_manual(values = library_system_colors,name = "System") +
  theme_void() +
  coord_equal()

The sf (simple features) package

https://r-spatial.github.io/sf/

library(sf)

Simple features is a set of standards for defining two-dimensional geometries, used by various GIS systems, building up from x-y coordinates within a coordinate reference system (crs).

sf package in R lets you:

You can make an sf object directly from a data frame:

five_points_sf <- five_points %>% st_as_sf(coords = c("var1","var2")) 

five_points_sf %>% print()
Simple feature collection with 5 features and 2 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 1 ymin: 1 xmax: 5 ymax: 5
CRS:           NA
  var1_odd_or_even var2_odd_or_even    geometry
1              odd              odd POINT (1 3)
2             even              odd POINT (2 5)
3              odd             even POINT (3 4)
4             even             even POINT (4 4)
5              odd              odd POINT (5 1)
five_points_sf %>% 
  ggplot() + 
  geom_sf(
    aes(color = var1_odd_or_even,
              fill = var2_odd_or_even),
          size = 5,
          shape = 21,
          stroke = 2
    )

Map Polygons

But for mapping we usually import geometry data from an external dataset.

Formats:

  • GEOJSON

  • ESRI shapefile

  • Many others (st_drivers(what = "vector"))

NYC geography resources:

https://www1.nyc.gov/site/planning/data-maps/open-data/census-download-metadata.page

  • Get the NYC PUMA (Public Use Microdata Area) geographies in GEOJSON format (PUMAs in NYC correspond - mostly - to Community Districts)
#read geojson format directly from download link
pumas_2010_geojson <- st_read("https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_Public_Use_Microdata_Areas_PUMAs_2010/FeatureServer/0/query?where=1=1&outFields=*&outSR=4326&f=pgeojson") %>% 
  st_as_sf()
Reading layer `OGRGeoJSON' from data source 
  `https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_Public_Use_Microdata_Areas_PUMAs_2010/FeatureServer/0/query?where=1=1&outFields=*&outSR=4326&f=pgeojson' 
  using driver `GeoJSON'
Simple feature collection with 55 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.25559 ymin: 40.49612 xmax: -73.70001 ymax: 40.91554
Geodetic CRS:  WGS 84
pumas_2010_geojson %>% print()
Simple feature collection with 55 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.25559 ymin: 40.49612 xmax: -73.70001 ymax: 40.91554
Geodetic CRS:  WGS 84
First 10 features:
   OBJECTID PUMA Shape__Area Shape__Length                       geometry
1         1 3701    97928415      53226.65 MULTIPOLYGON (((-73.89641 4...
2         2 3702   188993596     106167.48 MULTIPOLYGON (((-73.86477 4...
3         3 3703   267645154     305260.19 MULTIPOLYGON (((-73.78833 4...
4         4 3704   106217077      47970.15 MULTIPOLYGON (((-73.84793 4...
5         5 3705   122483734      68697.43 MULTIPOLYGON (((-73.87046 4...
6         6 3706    43887042      51825.93 MULTIPOLYGON (((-73.87773 4...
7         7 3707    42281072      37374.60 MULTIPOLYGON (((-73.89964 4...
8         8 3708    55881008      35002.58 MULTIPOLYGON (((-73.92478 4...
9         9 3709   124117798      73287.89 MULTIPOLYGON (((-73.83668 4...
10       10 3710   137760355      90067.74 MULTIPOLYGON (((-73.89681 4...
  • Shapefile format is also very common
#for shapefiles, download the zipped shapefile directory into your current working directory, then unzip (can do this outside R if you prefer!)
#download
download.file(url = "https://www1.nyc.gov/assets/planning/download/zip/data-maps/open-data/nypuma2010_21c.zip",destfile = "nypuma2010_21c.zip")
#unzip
unzip("nypuma2010_21c.zip")

#read in the shapefile
pumas_2010_shp <- st_read("nypuma2010_21c/nypuma2010.shp") %>% st_as_sf()
Reading layer `nypuma2010' from data source `/Users/sarahrankin/Documents/Mapping/nypuma2010_21c/nypuma2010.shp' using driver `ESRI Shapefile'
Simple feature collection with 55 features and 3 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 913175.1 ymin: 120121.9 xmax: 1067383 ymax: 272844.3
Projected CRS: NAD83 / New York Long Island (ftUS)
pumas_2010_shp %>% print()
Simple feature collection with 55 features and 3 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 913175.1 ymin: 120121.9 xmax: 1067383 ymax: 272844.3
Projected CRS: NAD83 / New York Long Island (ftUS)
First 10 features:
   PUMA Shape_Leng Shape_Area                       geometry
1  3701   53227.11   97928517 MULTIPOLYGON (((1012885 268...
2  3702  106167.59  188993632 MULTIPOLYGON (((1021632 267...
3  3703  305269.14  267643637 MULTIPOLYGON (((1042822 243...
4  3704   47970.20  106216909 MULTIPOLYGON (((1026309 256...
5  3705   68697.60  122483690 MULTIPOLYGON (((1020081 255...
6  3706   51826.07   43886931 MULTIPOLYGON (((1018060 261...
7  3707   37374.60   42281072 MULTIPOLYGON (((1012009 253...
8  3708   35002.71   55881068 MULTIPOLYGON (((1005061 247...
9  3709   73288.78  124117768 MULTIPOLYGON (((1029456 237...
10 3710   90067.96  137760290 MULTIPOLYGON (((1012822 229...
  • Again, adding geom_sf() to a ggplot renders the data in the geometry column - in this case it draws polygons because the geometry type is multipolygon
pumas_2010_geojson %>% 
  ggplot() + 
  geom_sf() 

  • Add our points to this map by specifying different data sources within the geom_sf and geom_point calls
  ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    fill = "grey",
    color = "grey"
    ) +
  geom_point(data = public_libraries, 
             aes(x=longitude,
                 y=latitude,
                 color = overagency)) +
  scale_color_manual(name = "System",values = library_system_colors) +
  theme_void()

  • sf objects can have a CRS (coordinate reference system) defined

  • What if our point data doesn’t match our CRS?

#same plot but with the shapefile data rather than geoJSON
  ggplot() + 
  geom_sf(data = pumas_2010_shp,
    fill = "grey",
    color = "grey"
    ) +
  geom_point(data = public_libraries, 
             aes(x=longitude,
                 y=latitude,
                 color = overagency)) +
  scale_color_manual(name = "System",values = library_system_colors) +
  theme_void()

  • We had x and y datapoints in our original data
  ggplot() + 
  geom_sf(data = pumas_2010_shp,
    fill = "grey",
    color = "grey"
    ) +
  geom_point(data = public_libraries, 
             aes(x=as.numeric(xcoord),
                 y=as.numeric(ycoord),
                 color = overagency)) +
  scale_color_manual(name = "System",values = library_system_colors) +
  theme_void()

  • Or transform the CRS using st_transform and st_crs
  ggplot() + 
  geom_sf(data = pumas_2010_shp %>% st_transform(crs = st_crs(pumas_2010_geojson)),
    fill = "grey",
    color = "grey"
    ) +
  geom_point(data = public_libraries, 
             aes(x=longitude,
                 y=latitude,
                 color = overagency)) +
  scale_color_manual(name = "System",values = library_system_colors) +
  theme_void()

Chloropleths

  • Map areas colored by data value

Broadband adoption in New York City

broadband_use <- read_csv("https://data.cityofnewyork.us/api/views/g5ah-i2sh/rows.csv?accessType=DOWNLOAD")

── Column specification ────────────────────────────────────────────────────────────────────────────────────────
cols(
  `PUMA (Public Use Microdata Sample Areas)` = col_double(),
  Borough = col_character(),
  `Home Broadband and Mobile Broadband Adoption (Percentage of  Households)` = col_double(),
  `Home Broadband and Mobile Broadband Adoption by Quartiles (High, Medium-High, Medium-Low, Low)` = col_character()
)
#clean up the names a bit
broadband_use <- broadband_use %>% 
  rename(PUMA = `PUMA (Public Use Microdata Sample Areas)`,
         broadband_adoption = `Home Broadband and Mobile Broadband Adoption (Percentage of  Households)`,
         broadband_adoption_quartile = `Home Broadband and Mobile Broadband Adoption by Quartiles (High, Medium-High, Medium-Low, Low)`)

broadband_use %>% head()
  • Add the broadband data to our PUMA geom dataset
#convert PUMA to string; make adoption quartile into factor so it'll sort correctly  
broadband_use <- broadband_use %>% 
  mutate(PUMA = as.character(PUMA),
         broadband_adoption_quartile = factor(broadband_adoption_quartile,
                                              levels = c("High", "Medium High", "Medium Low", "Low")))

pumas_2010_geojson <- pumas_2010_geojson %>% 
  left_join(broadband_use, by = "PUMA")

pumas_2010_geojson %>% print()
Simple feature collection with 55 features and 7 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -74.25559 ymin: 40.49612 xmax: -73.70001 ymax: 40.91554
Geodetic CRS:  WGS 84
First 10 features:
   OBJECTID PUMA Shape__Area Shape__Length Borough broadband_adoption broadband_adoption_quartile
1         1 3701    97928415      53226.65   Bronx               0.46                        High
2         2 3702   188993596     106167.48   Bronx               0.30                         Low
3         3 3703   267645154     305260.19   Bronx               0.34                  Medium Low
4         4 3704   106217077      47970.15   Bronx               0.30                         Low
5         5 3705   122483734      68697.43   Bronx               0.32                         Low
6         6 3706    43887042      51825.93   Bronx               0.39                 Medium High
7         7 3707    42281072      37374.60   Bronx               0.42                        High
8         8 3708    55881008      35002.58   Bronx               0.40                 Medium High
9         9 3709   124117798      73287.89   Bronx               0.29                         Low
10       10 3710   137760355      90067.74   Bronx               0.34                  Medium Low
                         geometry
1  MULTIPOLYGON (((-73.89641 4...
2  MULTIPOLYGON (((-73.86477 4...
3  MULTIPOLYGON (((-73.78833 4...
4  MULTIPOLYGON (((-73.84793 4...
5  MULTIPOLYGON (((-73.87046 4...
6  MULTIPOLYGON (((-73.87773 4...
7  MULTIPOLYGON (((-73.89964 4...
8  MULTIPOLYGON (((-73.92478 4...
9  MULTIPOLYGON (((-73.83668 4...
10 MULTIPOLYGON (((-73.89681 4...
  • Map the geom_sf fill aesthetic to broadband_adoption

  • Use scale_fill_gradient to specify fill colors

  ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    aes(fill = broadband_adoption),
    color = "grey"
    ) +
  geom_point(data = public_libraries,
             aes(x=longitude,
                 y=latitude,
                 color = overagency),
             size = 2) +
  scale_color_manual(name = "System",values = library_system_colors) +
  scale_fill_gradient(low = "grey70",high = "grey10", labels = scales::percent_format(accuracy = 1), name = "Broadband adoption") +
  coord_sf() +
  theme_void()

  • Or define discrete colors and use scale_fill_manual
broadband_cols <- c("grey10","grey30","grey50","grey70") %>% set_names(levels(pumas_2010_geojson$broadband_adoption_quartile))


ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    aes(fill = broadband_adoption_quartile),
    color = "grey"
    ) +
  geom_point(data = public_libraries,
             aes(x=longitude,
                 y=latitude,
                 color = overagency),
             size = 2) +
  scale_color_manual(name = "System",values = library_system_colors) +
  scale_fill_manual(values = broadband_cols, name = "Broadband adoption") +
  coord_sf() +
  theme_void()

Median income

https://www1.nyc.gov/site/planning/planning-level/nyc-population/american-community-survey.page

  • Download data and add it to our pumas_2010_geojson data frame
download.file(url = "https://www1.nyc.gov/assets/planning/download/office/planning-level/nyc-population/acs/econ_2018_acs5yr_puma.xlsx",
              destfile = "econ_2018_acs5yr_puma.xlsx")

econdata_puma <- readxl::read_xlsx("econ_2018_acs5yr_puma.xlsx", sheet = "EconData")

pumas_2010_geojson <- pumas_2010_geojson %>% 
  left_join(econdata_puma %>% 
              select(PUMA = GeoID,GeogName,median_household_income = MdHHIncE) %>% 
              mutate(median_income_category = 
                       cut(median_household_income,
                           breaks = c(0,50000,75000,100000,150000),
                           labels = c("$0-50K","$50-75K","$75-100K","$100K+"))))
Joining, by = "PUMA"
  • Create a new color scale with some greens
median_income_category_cols <- c("#a9ccbc","#7fb29b","#4c7f68","#335545") %>% set_names(levels(pumas_2010_geojson$median_income_category))

ggplot() + 
  geom_sf(data = pumas_2010_geojson,
    aes(fill = median_income_category),
    color = "grey"
    ) +
  geom_point(data = public_libraries,
             aes(x=longitude,
                 y=latitude,
                 color = overagency),
             size = 2) +
  scale_color_manual(name = "System",values = library_system_colors) +
  scale_fill_manual(values = median_income_category_cols, name = "Median income") +
  coord_sf() +
  theme_void() 

Leaflet: interactive maps

library(leaflet)

Leaflet: open source javascript library for making interactive maps on various platforms

R has a leaflet package that lets you create leaflet “widgets” within R

Create a map, add tiles, set the view

leaflet() %>% 
  #addTiles() %>% 
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10)

Add our public library branches

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude
             )
pal_system <- colorFactor(palette = library_system_colors ,
                          domain = unique(public_libraries$overagency),
                          ordered = T)

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude,
             color = ~pal_system(overagency),
             fill = ~pal_system(overagency),
             radius = 100,
             label = ~facname,
             popup = ~address
             ) %>% 
  addLegend(position = "topleft",
            pal = pal_system, 
            values = unique(public_libraries$overagency))

Because we have a basemap, we don’t need the PUMA polygons to create a basic map. But we can add them if we want our chloropleths on the interactive map.

pal_broadband <- colorFactor(palette = broadband_cols, 
                             domain = levels(pumas_2010_geojson$broadband_adoption_quartile),
                             ordered = T)


leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_broadband(broadband_adoption_quartile),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                             "<br>", GeogName,
                             "<br>Broadband adoption: ",round(broadband_adoption*100,0),"%",
                             "<br>Broadband adoption category: ",broadband_adoption_quartile)) 

Combine it with the libraries and add a legend

leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_broadband(broadband_adoption_quartile),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                              "<br>", GeogName,
                             "<br>Broadband adoption: ",round(broadband_adoption*100,0),"%",
                             "<br>Broadband adoption category: ",broadband_adoption_quartile)) %>% 
    addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude,
             color = ~pal_system(overagency),
             fill = ~pal_system(overagency),
             radius = 100,
             label = ~facname,
             popup = ~address
             ) %>% 
  addLegend(position = "topleft",
            pal = pal_system, 
            values = public_libraries$overagency,
            title = "Library System") %>% 
  addLegend(position = "topleft",
            pal = pal_broadband, 
            values =  pumas_2010_geojson$broadband_adoption_quartile,
            opacity = .7,
            title = "Broadband Access")

Visualize more than one layer

pal_income <- colorFactor(palette = median_income_category_cols, 
                             domain = levels(pumas_2010_geojson$median_income_category),
                             ordered = T)

libraries_map <- leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lat = 40.7,lng = -74, zoom = 10) %>%
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_broadband(broadband_adoption_quartile),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                              "<br>", GeogName,
                             "<br>Broadband adoption: ",round(broadband_adoption*100,0),"%",
                             "<br>Broadband adoption category: ",broadband_adoption_quartile),
              group = "Broadband") %>% 
  addPolygons(data = pumas_2010_geojson,
              fillColor = ~pal_income(median_income_category),
              fillOpacity = .7,
              color = "grey",
              stroke = T,
              weight = 1,
              label = ~PUMA,
              popup = ~paste0("<b>PUMA ",PUMA,"</b>",
                              "<br>", GeogName,
                             "<br>Median income: $",format(median_household_income,big.mark = ","),
                             "<br>Median income category: ",median_income_category),
              group = "income") %>% 
    addCircles(data = public_libraries,
             lng = ~longitude,
             lat = ~latitude,
             color = ~pal_system(overagency),
             fill = ~pal_system(overagency),
             opacity = 1,
             fillOpacity = 1,
             radius = 100,
             label = ~facname,
             popup = ~address#,
             #group = "Broadband"
             ) %>% 
    # addCircles(data = public_libraries,
    #          lng = ~longitude,
    #          lat = ~latitude,
    #          color = ~pal_system(overagency),
    #          fill = ~pal_system(overagency),
    #          opacity = 1,
    #          fillOpacity = 1,
    #          radius = 100,
    #          label = ~facname,
    #          popup = ~address,
    #          group = "Income"
    #          ) %>% 
  addLegend(position = "topleft",
            pal = pal_system, 
            values = public_libraries$overagency,
            title = "Library System") %>% 
  addLegend(position = "topleft",
            pal = pal_broadband, 
            values =  pumas_2010_geojson$broadband_adoption_quartile,
            opacity = .7,
            title = "Broadband Access",
            group = "Broadband") %>% 
  addLegend(position = "topleft",
            pal = pal_income, 
            values =  pumas_2010_geojson$median_income_category,
            opacity = .7,
            title = "Median household income",
            group = "Income") %>% 
  addLayersControl(
            baseGroups = c("Broadband","Income"),
            options = layersControlOptions(collapsed = F)#,
            #overlayGroups = c("Branches")
  ) 


libraries_map

The htmlwidgets package lets you save this map as an html file, which can then be opened in a browser or embedded in a web page

htmlwidgets::saveWidget(libraries_map,
                        file = "libraries_map.html",
                        title = "NYC Libraries:  Broadband Access and Median Income")

Other resources

LS0tCnRpdGxlOiAiTWFraW5nIE1hcHMgaW4gUiIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKZWRpdG9yX29wdGlvbnM6CiAgY2h1bmtfb3V0cHV0X3R5cGU6IGlubGluZQotLS0KCnwgKkFuRXggTGVhcm5pbmcgU3VtbWl0Kgp8ICpUaHVyc2RheSBPY3RvYmVyIDIxLCAyMDIxKgp8IAp8ICpTYXJhaCBSYW5raW4qCnwgKlF1YW50aXRhdGl2ZSBEYXRhIEFuYWx5c3QqCnwgKlN0YXRlZ3kgYW5kIFB1YmxpYyBJbXBhY3QqCnwgKk5ldyBZb3JrIFB1YmxpYyBMaWJyYXJ5Kgp8ICpzYXJhaHJhbmtpblxAbnlwbC5vcmcqCnwgCgojIyBUb3BpY3MKCi0gICBTdGF0aWMgbWFwcyB3aXRoIHNmIGFuZCBnZ3Bsb3QKLSAgIER5bmFtaWMgbWFwcyB3aXRoIGxlYWZsZXQKCiMjIFNldHVwCgpUaGlzIGlzIGFuIFJtYXJrZG93biBub3RlYm9vay4gSWYgeW91J3JlIHZpZXdpbmcgaXQgaW4gYSB3ZWIgYnJvd3NlciwgeW91IGNhbiBjbGljayBvbiBDb2RlLVw+RG93bmxvYWQgUm1kIGluIHRoZSB0b3AgcmlnaHQgY29ybmVyLCB0aGVuIG9wZW4gaXQgaW4gUlN0dWRpbyBhbmQgdG8gcnVuL2VkaXQgdGhlIGNvZGUuIElmIHlvdSBkb24ndCB1c2UgUlN0dWRpbyB5b3UgY2FuIGFsc28ganVzdCBjb3B5IHRoZSBjb2RlIGludG8geW91ciBlZGl0b3Igb2YgY2hvaWNlLgoKV2UnbGwgYmUgdXNpbmcgYSB2YXJpZXR5IG9mIGB0aWR5dmVyc2VgIHBhY2thZ2VzIGFzIHdlbGwgYXMgYHNmYCBhbmQgYGxlYWZsZXRgLiBJZiB5b3UgZG9uJ3QgaGF2ZSB0aGVzZSBwYWNrYWdlcyBpbnN0YWxsZWQsIHVuY29tbWVudCB0aGUgZm9sbG93aW5nIGNvZGUgYW5kIGluc3RhbGwgdGhlbSBub3cuIChJdCdsbCB0YWtlIGEgbGl0dGxlIHdoaWxlLikKCmBgYHtyfQojIGluc3RhbGwucGFja2FnZXMoInRpZHl2ZXJzZSIpCiMgaW5zdGFsbC5wYWNrYWdlcygic2YiKQojIGluc3RhbGwucGFja2FnZXMoImxlYWZsZXQiKQoKYGBgCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQojbG9hZCBqdXN0IHRoZSB0aWR5dmVyc2UgZm9yIG5vdwpsaWJyYXJ5KHRpZHl2ZXJzZSkKCmBgYAoKIyMgUXVpY2sgZ2dwbG90IHJlZnJlc2hlcgoKTGV0J3MgY3JlYXRlIHNvbWUgdmVyeSBzaW1wbGUgcG9pbnQgZGF0YS4KCmBgYHtyfQpmaXZlX3BvaW50cyA8LSBkYXRhLmZyYW1lKHZhcjEgPSAxOjUsIHZhcjIgPSBhcy5pbnRlZ2VyKGMoMyw1LDQsNCwxKSkpCmZpdmVfcG9pbnRzIApgYGAKCi0gICBDYWxsIGBnZ3Bsb3QoKWAgdG8gbWFrZSBhIGNhbnZhcwoKLSAgIERlZmluZSBhZXN0aGV0aWNzIHdpdGggYGFlcygpYCAtIHRoZXNlIG1hcCB5b3VyIGRhdGEgdG8gc3BhdGlhbCBwcm9wZXJ0aWVzIGxpa2UgdGhlIHggYW5kIHkgZGltZW5zaW9ucywgb3IgdmlzdWFsIGFlc3RoZXRpY3MgbGlrZSBjb2xvciwgZmlsbCwgYWxwaGEgZXRjCgotICAgQWRkIGEgZ2VvbSAtIGhlcmUsIGBnZW9tX3BvaW50KClgCgpgYGB7cn0KZml2ZV9wb2ludHMgJT4lIAogIGdncGxvdChhZXMoeD12YXIxLCB5PXZhcjIpKSArCiAgZ2VvbV9wb2ludCgpCiAgCmBgYAoKLSAgIEFkZCBmbGFncyBpbmRpY2F0aW5nIHdoZXRoZXIgZWFjaCB2YXJpYWJsZSBpcyBldmVuIG9yIG9kZCAoYCUlYCBpcyB0aGUgbW9kdWxvIGZ1bmN0aW9uKQoKYGBge3J9CmZpdmVfcG9pbnRzIDwtIGZpdmVfcG9pbnRzICU+JSAgCiAgbXV0YXRlKHZhcjFfb2RkX29yX2V2ZW4gPSBpZmVsc2UodmFyMSUlMj09MCwiZXZlbiIsIm9kZCIpLAogICAgICAgICB2YXIyX29kZF9vcl9ldmVuID0gaWZlbHNlKHZhcjIlJTI9PTAsImV2ZW4iLCJvZGQiKSkKCmZpdmVfcG9pbnRzCgoKYGBgCgotICAgTWFwICJ2YXJfMVxfaXMgZXZlbiIgdG8gdGhlIGNvbG9yIGFlc3RoZXRpYwoKYGBge3J9CmZpdmVfcG9pbnRzICU+JSAKICBnZ3Bsb3QoYWVzKHg9dmFyMSwgeT12YXIyLGNvbG9yID0gdmFyMV9vZGRfb3JfZXZlbikpICsKICBnZW9tX3BvaW50KHNpemUgPSA1KSAKCgpgYGAKCiMjIyMgQWVzdGhldGljcyBpbiBnZ3Bsb3QKCi0gICBDb2xvciBpcyBmb3IgbGluZXMgKGluY2x1ZGluZyBvdXRsaW5lcyBvZiBzaGFwZXMpCgotICAgRmlsbCBpcyBmb3IgYXJlYXMgKGZpbGxpbmcgaW4gc2hhcGVzKQoKICAgIC0gICBidXQgb3VyIHBvaW50cyBhcmUgZmlsbGVkIGluIGJhc2VkIG9uIHRoZSBjb2xvciBhZXN0aGV0aWM/IQoKICAgIC0gICB0aGUgZGVmYXVsdCBwb2ludCBzaGFwZSBpcyBzaGFwZSAxOSAoImNpcmNsZSIpIC0ganVzdCBhIHNvbGlkIGNpcmNsZSB3aXRoIHRoZSBjb2xvciB0YWtlbiBmcm9tIHRoZSBjb2xvciBhZXN0aGV0aWM7IHNoYXBlIDIxICgiZmlsbGVkIGNpcmNsZSIpIGlzIGEgY2lyY2xlIHdpdGggYSBmaWxsIGFuZCBhbiBvdXRsaW5lLCB3aGljaCBjYW4gYmUgc3BlY2lmaWVkIHNlcGFyYXRlbHkKCi0gICBHZW5lcmFsbHksIHlvdSBjYW4gZWl0aGVyIG1hcCB0aGUgYWVzdGhldGljIHRvIGEgZGF0YSBwb2ludCBvciBzcGVjaWZ5IGl0cyB2YWx1ZSBvdXRzaWRlIG9mIHRoZSBgYWVzKClgIGZ1bmN0aW9uLiBJZiB5b3UgZG8gbmVpdGhlciwgYSBkZWZhdWx0IHdpbGwgYmUgdXNlZC4KCi0gICBBZXN0aGV0aWMgbWFwcGluZy9zcGVjaWZpY2F0aW9uIGNhbiBiZSBkb25lIGluIHRoZSBvcmlnaW5hbCBnZ3Bsb3QoKSBjYWxsLCBvciB3aXRoaW4gaW5kaXZpZHVhbCBnZW9tcwoKR29vZCBnZW5lcmFsIHJlZmVyZW5jZSBvbiBzcGVjaWZ5aW5nIGFlc3RoZXRpY3M6Cgo8aHR0cHM6Ly9nZ3Bsb3QyLnRpZHl2ZXJzZS5vcmcvYXJ0aWNsZXMvZ2dwbG90Mi1zcGVjcy5odG1sI3BvaW50LTE+CgpgYGB7cn0KZml2ZV9wb2ludHMgJT4lIAogIGdncGxvdChhZXMoeD12YXIxLCAKICAgICAgICAgICAgIHk9dmFyMiwKICAgICAgICAgICAgIGNvbG9yID0gdmFyMV9vZGRfb3JfZXZlbiwKICAgICAgICAgICAgIGZpbGwgPSB2YXIyX29kZF9vcl9ldmVuKSkgKwogIGdlb21fcG9pbnQoc2l6ZSA9IDUsIAogICAgICAgICAgICAgc2hhcGUgPSAyMSwKICAgICAgICAgICAgIHN0cm9rZSA9IDIpIAoKYGBgCgojIyMgUG9pbnRzIGFzIE1hcHMKCi0gICBMb25naXR1ZGUgYW5kIExhdGl0dWRlIGFyZSAob24gYSAyZCBtYXApIHggYW5kIHkgY29vcmRpbmF0ZXMKCi0gICBMZXQncyBnZXQgc29tZSBjb29yZGluYXRlIGRhdGEgZnJvbSB0aGUgTllDIGZhY2lsaXRpZXMgZGF0YWJhc2UKCjxodHRwczovL2RhdGEuY2l0eW9mbmV3eW9yay51cy9DaXR5LUdvdmVybm1lbnQvRmFjaWxpdGllcy1EYXRhYmFzZS9qaTgyLXhiYTU+CgpgYGB7cn0KcHVibGljX2xpYnJhcmllcyA8LSBqc29ubGl0ZTo6ZnJvbUpTT04oImh0dHBzOi8vZGF0YS5jaXR5b2ZuZXd5b3JrLnVzL3Jlc291cmNlL2ppODIteGJhNS5qc29uP2ZhY3R5cGU9UFVCTElDJTIwTElCUkFSWSIpIAoKcHVibGljX2xpYnJhcmllcyAlPiUgc3RyKCkKYGBgCgpXZSBuZWVkIGxhdGl0dWRlIGFuZCBsb25naXR1ZGUgdG8gYmUgbnVtZXJpYywgbm90IHN0cmluZ3MKCmBgYHtyfQpwdWJsaWNfbGlicmFyaWVzIDwtIHB1YmxpY19saWJyYXJpZXMgJT4lICAKICBtdXRhdGUobGF0aXR1ZGU9YXMubnVtZXJpYyhsYXRpdHVkZSksbG9uZ2l0dWRlPWFzLm51bWVyaWMobG9uZ2l0dWRlKSkKCnB1YmxpY19saWJyYXJpZXMgJT4lIAogIHNlbGVjdChmYWNuYW1lLGxhdGl0dWRlLGxvbmdpdHVkZSkgJT4lIAogIHN0cigpCgoKYGBgCgotICAgTWFwIHRoZSBsb25naXR1ZGUgYW5kIGxhdGl0dWRlIHRvIHRoZSB4IGFuZCB5IGFlc3RoZXRpY3MKCiAgICAtICAgRXZlcnlvbmUgaGFzIGEgc3RyYXRlZ3kgZm9yIHJlbWVtYmVyaW5nIHdoaWNoIGlzIHdoaWNoIC0gSSBsaWtlIHRvIHRoaW5rIG9mIHNvdXAgZHVtcGxpbmdzIC0gIioqWCoqaWFvICoqbG9uZyoqIGJhbyIgLSBjcmVkaXQgdG8gW1xAc2Vhbmtyb3NzXShodHRwczovL3R3aXR0ZXIuY29tL3NlYW5rcm9zcy9zdGF0dXMvMTEzNDMyNjcyODQ3NjcxMjk2MCkKCmBgYHtyfQoKcHVibGljX2xpYnJhcmllcyAlPiUgCiAgZ2dwbG90KGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgIHk9bGF0aXR1ZGUpKSArIAogIGdlb21fcG9pbnQoKSAKCgoKYGBgCgotICAgVGhhdCBkb2VzIG5vdCBsb29rIGxpa2UgYSBtYXAuIExldCdzIGxvb2sgYXQgb3VyIGRhdGEuCgpgYGB7cn0KcHVibGljX2xpYnJhcmllcyAlPiUgCiAgc2VsZWN0KGxhdGl0dWRlLGxvbmdpdHVkZSkgJT4lIAogIHN1bW1hcnkoKQoKCmBgYAoKLSAgIEF0IGxlYXN0IHNvbWUgb2Ygb3VyIGxhdC9sb25nIHZhbHVlcyBhcmUgd2F5IG91dCBvZiBleHBlY3RlZCByYW5nZQoKYGBge3J9CnB1YmxpY19saWJyYXJpZXMgJT4lIAogIGZpbHRlcihsYXRpdHVkZTwzOXxsb25naXR1ZGU+NzQpICU+JSAKICBzZWxlY3QobGF0aXR1ZGUsbG9uZ2l0dWRlLGZhY25hbWUsYWRkcmVzc251bSxzdHJlZXRuYW1lLGFkZHJlc3MsY2l0eSxib3JvKQoKCmBgYAoKLSAgIExvb2tzIGxpa2Ugd2UgY2FuIGNsZWFuIHRoaXMgdXAgYnkganVzdCBmaWx0ZXJpbmcgb3V0IHRoZXNlIHR3byBjYXNlcwoKYGBge3J9CgogcHVibGljX2xpYnJhcmllcyA8LSBwdWJsaWNfbGlicmFyaWVzICU+JSAgIAogIGZpbHRlcighbGF0aXR1ZGU9PTAsCiAgICAgICAgICFsb25naXR1ZGU9PTApCiAKIHB1YmxpY19saWJyYXJpZXMgJT4lIG5yb3coKQoKYGBgCgotICAgVHJ5IGFnYWluCgpgYGB7cn0KcHVibGljX2xpYnJhcmllcyAlPiUgCiAgZ2dwbG90KGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgIHk9bGF0aXR1ZGUpKSArIAogIGdlb21fcG9pbnQoKSAKCmBgYAoKLSAgIE1ha2UgaXQgYSBiaXQgcHJldHRpZXIKCiAgICAtICAgQ29sb3IgdGhlIHBvaW50cyBhbmQgcmVuYW1lIHRoZSBsZWdlbmQgd2l0aCBgc2NhbGVfY29sb3JfbWFudWFsKClgCgogICAgLSAgIEdldCByaWQgb2YgYXhlcyBhbmQgYmFja2dyb3VuZCB3aXRoIGB0aGVtZV92b2lkKClgCgpgYGB7cn0KI2RlZmluZSBzb21lIHN5c3RlbSBjb2xvcnMgLSBhIG5hbWVkIHZlY3RvciBvZiBjb2xvcnMgd2lsbCBtYXAgdGhlIGNvbG9ycyB0byB0aGUgdmFsdWVzIG9mIHRoZSBjb2xvciBhZXN0aGV0aWMgKGJhY2t0aWNrcyBsZXQgeW91IHVzZSBub24tc3ludGFjdGljIFIgbmFtZXMpCiNjb2xvciB2YWx1ZXMgY2FuIGJlIG5hbWVzIGZyb20gUidzIGJ1aWx0LWluIGNvbG9ycyBvciBoZXggY29kZXMKCmxpYnJhcnlfc3lzdGVtX2NvbG9ycyA8LSBjKGBOZXcgWW9yayBQdWJsaWMgTGlicmFyeWA9InJlZDMiLAogICAgICAgICAgICAgICAgICAgYEJyb29rbHluIFB1YmxpYyBMaWJyYXJ5YCA9ICJvcmFuZ2UyIiwKICAgICAgICAgICAgICAgICAgIGBRdWVlbnMgUHVibGljIExpYnJhcnlgID0gInB1cnBsZTMiKQoKcHVibGljX2xpYnJhcmllcyAlPiUgCiAgZ2dwbG90KGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgIHk9bGF0aXR1ZGUsCiAgICAgICAgICAgICBjb2xvciA9IG92ZXJhZ2VuY3kpKSArIAogIGdlb21fcG9pbnQoKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycyxuYW1lID0gIlN5c3RlbSIpICsKICB0aGVtZV92b2lkKCkgKwogIGNvb3JkX2VxdWFsKCkKCgoKYGBgCgojIyBUaGUgYHNmYCAoc2ltcGxlIGZlYXR1cmVzKSBwYWNrYWdlCgo8aHR0cHM6Ly9yLXNwYXRpYWwuZ2l0aHViLmlvL3NmLz4KCmBgYHtyIH0KbGlicmFyeShzZikKYGBgCgpTaW1wbGUgZmVhdHVyZXMgaXMgYSBzZXQgb2Ygc3RhbmRhcmRzIGZvciBkZWZpbmluZyB0d28tZGltZW5zaW9uYWwgZ2VvbWV0cmllcywgdXNlZCBieSB2YXJpb3VzIEdJUyBzeXN0ZW1zLCBidWlsZGluZyB1cCBmcm9tIHgteSBjb29yZGluYXRlcyB3aXRoaW4gYSBjb29yZGluYXRlIHJlZmVyZW5jZSBzeXN0ZW0gKGNycykuCgotICAgUE9JTlQKCi0gICBMSU5FU1RSSU5HCgotICAgUE9MWUdPTgoKLSAgIE1VTFRJUE9JTlQKCi0gICBNVUxUSVBPTFlHT04KCmBzZmAgcGFja2FnZSBpbiBSIGxldHMgeW91OgoKLSAgIHN0b3JlIGdlb21ldHJ5IGRhdGEgYXMgYSBsaXN0LWNvbHVtbiBpbiBhIGRhdGEgZnJhbWUgb3IgdGliYmxlLCBhbG9uZ3NpZGUgb3RoZXIgZGF0YSBjb2x1bW5zCgotICAgaW1wb3J0IGRhdGEgaW50byB0aGUgUiBzZiBmb3JtYXQgZnJvbSB2YXJpb3VzIEdJUyBmb3JtYXRzCgotICAgbWFuaXB1bGF0ZSBzZiBnZW9tZXRyaWVzIChjb25jYXRlbmF0ZSwgc3VidHJhY3QsIGZpbmQgb3ZlcmxhcHMsIGV0YykKCllvdSBjYW4gbWFrZSBhbiBgc2ZgIG9iamVjdCBkaXJlY3RseSBmcm9tIGEgZGF0YSBmcmFtZToKCmBgYHtyfQoKZml2ZV9wb2ludHNfc2YgPC0gZml2ZV9wb2ludHMgJT4lIHN0X2FzX3NmKGNvb3JkcyA9IGMoInZhcjEiLCJ2YXIyIikpIAoKZml2ZV9wb2ludHNfc2YgJT4lIHByaW50KCkKYGBgCgotICAgVGhlcmUgaXMgYSBzcGVjaWFsIGdlb20gZm9yIHNmIG9iamVjdHM6IGBnZW9tX3NmYAoKLSAgIFdoZW4geW91IGFkZCBnZW9tX3NmIHRvIGEgcGxvdCAoYW5kIHRoZSBgc2ZgIHBhY2thZ2UgaXMgbG9hZGVkKSwgZ2dwbG90IHdpbGwgcGxvdCB0aGUgZ2VvbWV0cnkgY29sdW1uIC0gcGxvdHRpbmcgbWV0aG9kIGRlcGVuZHMgb24gdGhlIHNmIGdlb21ldHJ5IHR5cGUgKHBvaW50LCBwb2x5Z29uLCBldGMpCgpgYGB7cn0KCmZpdmVfcG9pbnRzX3NmICU+JSAKICBnZ3Bsb3QoKSArIAogIGdlb21fc2YoCiAgICBhZXMoY29sb3IgPSB2YXIxX29kZF9vcl9ldmVuLAogICAgICAgICAgICAgIGZpbGwgPSB2YXIyX29kZF9vcl9ldmVuKSwKICAgICAgICAgIHNpemUgPSA1LAogICAgICAgICAgc2hhcGUgPSAyMSwKICAgICAgICAgIHN0cm9rZSA9IDIKICAgICkKCgpgYGAKCiMjIyBNYXAgUG9seWdvbnMKCkJ1dCBmb3IgbWFwcGluZyB3ZSB1c3VhbGx5IGltcG9ydCBnZW9tZXRyeSBkYXRhIGZyb20gYW4gZXh0ZXJuYWwgZGF0YXNldC4KCkZvcm1hdHM6CgotICAgR0VPSlNPTgoKLSAgIEVTUkkgc2hhcGVmaWxlCgotICAgTWFueSBvdGhlcnMgKGBzdF9kcml2ZXJzKHdoYXQgPSAidmVjdG9yIilgKQoKTllDIGdlb2dyYXBoeSByZXNvdXJjZXM6Cgo8aHR0cHM6Ly93d3cxLm55Yy5nb3Yvc2l0ZS9wbGFubmluZy9kYXRhLW1hcHMvb3Blbi1kYXRhL2NlbnN1cy1kb3dubG9hZC1tZXRhZGF0YS5wYWdlPgoKLSAgIEdldCB0aGUgTllDIFBVTUEgKFB1YmxpYyBVc2UgTWljcm9kYXRhIEFyZWEpIGdlb2dyYXBoaWVzIGluIEdFT0pTT04gZm9ybWF0IChQVU1BcyBpbiBOWUMgY29ycmVzcG9uZCAtIG1vc3RseSAtIHRvIENvbW11bml0eSBEaXN0cmljdHMpCgpgYGB7ciBjYWNoZT1UUlVFfQoKI3JlYWQgZ2VvanNvbiBmb3JtYXQgZGlyZWN0bHkgZnJvbSBkb3dubG9hZCBsaW5rCnB1bWFzXzIwMTBfZ2VvanNvbiA8LSBzdF9yZWFkKCJodHRwczovL3NlcnZpY2VzNS5hcmNnaXMuY29tL0dmd1dOa2hPajliTkJxb0ovYXJjZ2lzL3Jlc3Qvc2VydmljZXMvTllDX1B1YmxpY19Vc2VfTWljcm9kYXRhX0FyZWFzX1BVTUFzXzIwMTAvRmVhdHVyZVNlcnZlci8wL3F1ZXJ5P3doZXJlPTE9MSZvdXRGaWVsZHM9KiZvdXRTUj00MzI2JmY9cGdlb2pzb24iKSAlPiUgCiAgc3RfYXNfc2YoKQoKcHVtYXNfMjAxMF9nZW9qc29uICU+JSBwcmludCgpCgogCgoKYGBgCgotICAgU2hhcGVmaWxlIGZvcm1hdCBpcyBhbHNvIHZlcnkgY29tbW9uCgpgYGB7ciBjYWNoZT1UUlVFfQojZm9yIHNoYXBlZmlsZXMsIGRvd25sb2FkIHRoZSB6aXBwZWQgc2hhcGVmaWxlIGRpcmVjdG9yeSBpbnRvIHlvdXIgY3VycmVudCB3b3JraW5nIGRpcmVjdG9yeSwgdGhlbiB1bnppcCAoY2FuIGRvIHRoaXMgb3V0c2lkZSBSIGlmIHlvdSBwcmVmZXIhKQojZG93bmxvYWQKZG93bmxvYWQuZmlsZSh1cmwgPSAiaHR0cHM6Ly93d3cxLm55Yy5nb3YvYXNzZXRzL3BsYW5uaW5nL2Rvd25sb2FkL3ppcC9kYXRhLW1hcHMvb3Blbi1kYXRhL255cHVtYTIwMTBfMjFjLnppcCIsZGVzdGZpbGUgPSAibnlwdW1hMjAxMF8yMWMuemlwIikKI3VuemlwCnVuemlwKCJueXB1bWEyMDEwXzIxYy56aXAiKQoKI3JlYWQgaW4gdGhlIHNoYXBlZmlsZQpwdW1hc18yMDEwX3NocCA8LSBzdF9yZWFkKCJueXB1bWEyMDEwXzIxYy9ueXB1bWEyMDEwLnNocCIpICU+JSBzdF9hc19zZigpCgoKcHVtYXNfMjAxMF9zaHAgJT4lIHByaW50KCkKCmBgYAoKLSAgIEFnYWluLCBhZGRpbmcgYGdlb21fc2YoKWAgdG8gYSBnZ3Bsb3QgcmVuZGVycyB0aGUgZGF0YSBpbiB0aGUgZ2VvbWV0cnkgY29sdW1uIC0gaW4gdGhpcyBjYXNlIGl0IGRyYXdzIHBvbHlnb25zIGJlY2F1c2UgdGhlIGdlb21ldHJ5IHR5cGUgaXMgbXVsdGlwb2x5Z29uCgpgYGB7ciB9CnB1bWFzXzIwMTBfZ2VvanNvbiAlPiUgCiAgZ2dwbG90KCkgKyAKICBnZW9tX3NmKCkgCgpgYGAKCi0gICBBZGQgb3VyIHBvaW50cyB0byB0aGlzIG1hcCBieSBzcGVjaWZ5aW5nIGRpZmZlcmVudCBkYXRhIHNvdXJjZXMgd2l0aGluIHRoZSBgZ2VvbV9zZmAgYW5kIGBnZW9tX3BvaW50YCBjYWxscwoKYGBge3IgfQoKICBnZ3Bsb3QoKSArIAogIGdlb21fc2YoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgIGZpbGwgPSAiZ3JleSIsCiAgICBjb2xvciA9ICJncmV5IgogICAgKSArCiAgZ2VvbV9wb2ludChkYXRhID0gcHVibGljX2xpYnJhcmllcywgCiAgICAgICAgICAgICBhZXMoeD1sb25naXR1ZGUsCiAgICAgICAgICAgICAgICAgeT1sYXRpdHVkZSwKICAgICAgICAgICAgICAgICBjb2xvciA9IG92ZXJhZ2VuY3kpKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKG5hbWUgPSAiU3lzdGVtIix2YWx1ZXMgPSBsaWJyYXJ5X3N5c3RlbV9jb2xvcnMpICsKICB0aGVtZV92b2lkKCkKCgpgYGAKCi0gICBgc2ZgIG9iamVjdHMgY2FuIGhhdmUgYSBDUlMgKGNvb3JkaW5hdGUgcmVmZXJlbmNlIHN5c3RlbSkgZGVmaW5lZAoKLSAgIFdoYXQgaWYgb3VyIHBvaW50IGRhdGEgZG9lc24ndCBtYXRjaCBvdXIgQ1JTPwoKYGBge3J9CiNzYW1lIHBsb3QgYnV0IHdpdGggdGhlIHNoYXBlZmlsZSBkYXRhIHJhdGhlciB0aGFuIGdlb0pTT04KICBnZ3Bsb3QoKSArIAogIGdlb21fc2YoZGF0YSA9IHB1bWFzXzIwMTBfc2hwLAogICAgZmlsbCA9ICJncmV5IiwKICAgIGNvbG9yID0gImdyZXkiCiAgICApICsKICBnZW9tX3BvaW50KGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLCAKICAgICAgICAgICAgIGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgICAgICB5PWxhdGl0dWRlLAogICAgICAgICAgICAgICAgIGNvbG9yID0gb3ZlcmFnZW5jeSkpICsKICBzY2FsZV9jb2xvcl9tYW51YWwobmFtZSA9ICJTeXN0ZW0iLHZhbHVlcyA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycykgKwogIHRoZW1lX3ZvaWQoKQoKYGBgCgotICAgV2UgaGFkIHggYW5kIHkgZGF0YXBvaW50cyBpbiBvdXIgb3JpZ2luYWwgZGF0YQoKYGBge3J9CgogIGdncGxvdCgpICsgCiAgZ2VvbV9zZihkYXRhID0gcHVtYXNfMjAxMF9zaHAsCiAgICBmaWxsID0gImdyZXkiLAogICAgY29sb3IgPSAiZ3JleSIKICAgICkgKwogIGdlb21fcG9pbnQoZGF0YSA9IHB1YmxpY19saWJyYXJpZXMsIAogICAgICAgICAgICAgYWVzKHg9YXMubnVtZXJpYyh4Y29vcmQpLAogICAgICAgICAgICAgICAgIHk9YXMubnVtZXJpYyh5Y29vcmQpLAogICAgICAgICAgICAgICAgIGNvbG9yID0gb3ZlcmFnZW5jeSkpICsKICBzY2FsZV9jb2xvcl9tYW51YWwobmFtZSA9ICJTeXN0ZW0iLHZhbHVlcyA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycykgKwogIHRoZW1lX3ZvaWQoKQoKYGBgCgotICAgT3IgdHJhbnNmb3JtIHRoZSBDUlMgdXNpbmcgYHN0X3RyYW5zZm9ybWAgYW5kIGBzdF9jcnNgCgpgYGB7cn0KICBnZ3Bsb3QoKSArIAogIGdlb21fc2YoZGF0YSA9IHB1bWFzXzIwMTBfc2hwICU+JSBzdF90cmFuc2Zvcm0oY3JzID0gc3RfY3JzKHB1bWFzXzIwMTBfZ2VvanNvbikpLAogICAgZmlsbCA9ICJncmV5IiwKICAgIGNvbG9yID0gImdyZXkiCiAgICApICsKICBnZW9tX3BvaW50KGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLCAKICAgICAgICAgICAgIGFlcyh4PWxvbmdpdHVkZSwKICAgICAgICAgICAgICAgICB5PWxhdGl0dWRlLAogICAgICAgICAgICAgICAgIGNvbG9yID0gb3ZlcmFnZW5jeSkpICsKICBzY2FsZV9jb2xvcl9tYW51YWwobmFtZSA9ICJTeXN0ZW0iLHZhbHVlcyA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycykgKwogIHRoZW1lX3ZvaWQoKQoKYGBgCgojIyMgQ2hsb3JvcGxldGhzCgotICAgTWFwIGFyZWFzIGNvbG9yZWQgYnkgZGF0YSB2YWx1ZQoKIyMjIyBCcm9hZGJhbmQgYWRvcHRpb24gaW4gTmV3IFlvcmsgQ2l0eQoKLSAgIE5ZQyBPcGVuIERhdGE6IFtOWUMncyBJbnRlcm5ldCBNYXN0ZXIgUGxhbjogSG9tZSBCcm9hZGJhbmQgYW5kIE1vYmlsZSBCcm9hZGJhbmQgQWRvcHRpb24gYnkgUFVNQV0oaHR0cHM6Ly9kYXRhLmNpdHlvZm5ld3lvcmsudXMvQ2l0eS1Hb3Zlcm5tZW50L0ludGVybmV0LU1hc3Rlci1QbGFuLUhvbWUtQnJvYWRiYW5kLWFuZC1Nb2JpbGUtQnJvL2c1YWgtaTJzaCkKCi0gICBCYXNlZCBvbiBBbWVyaWNhbiBDb21tdW5pdHkgU3VydmV5IERhdGE7IHNob3dzIHNoYXJlIG9mIGhvdXNlaG9sZHMgdGhhdCBoYXZlIGJvdGggbW9iaWxlIGFuZCBob21lIGJyb2FkYmFuZAoKYGBge3IgcGFnZWQucHJpbnQ9RkFMU0UsIGNhY2hlPVRSVUV9Cgpicm9hZGJhbmRfdXNlIDwtIHJlYWRfY3N2KCJodHRwczovL2RhdGEuY2l0eW9mbmV3eW9yay51cy9hcGkvdmlld3MvZzVhaC1pMnNoL3Jvd3MuY3N2P2FjY2Vzc1R5cGU9RE9XTkxPQUQiKQoKCiNjbGVhbiB1cCB0aGUgbmFtZXMgYSBiaXQKYnJvYWRiYW5kX3VzZSA8LSBicm9hZGJhbmRfdXNlICU+JSAKICByZW5hbWUoUFVNQSA9IGBQVU1BIChQdWJsaWMgVXNlIE1pY3JvZGF0YSBTYW1wbGUgQXJlYXMpYCwKICAgICAgICAgYnJvYWRiYW5kX2Fkb3B0aW9uID0gYEhvbWUgQnJvYWRiYW5kIGFuZCBNb2JpbGUgQnJvYWRiYW5kIEFkb3B0aW9uIChQZXJjZW50YWdlIG9mICBIb3VzZWhvbGRzKWAsCiAgICAgICAgIGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSA9IGBIb21lIEJyb2FkYmFuZCBhbmQgTW9iaWxlIEJyb2FkYmFuZCBBZG9wdGlvbiBieSBRdWFydGlsZXMgKEhpZ2gsIE1lZGl1bS1IaWdoLCBNZWRpdW0tTG93LCBMb3cpYCkKCmJyb2FkYmFuZF91c2UgJT4lIGhlYWQoKQoKCmBgYAoKLSAgIEFkZCB0aGUgYnJvYWRiYW5kIGRhdGEgdG8gb3VyIFBVTUEgZ2VvbSBkYXRhc2V0CgpgYGB7ciBjYWNoZT1UUlVFfQoKI2NvbnZlcnQgUFVNQSB0byBzdHJpbmc7IG1ha2UgYWRvcHRpb24gcXVhcnRpbGUgaW50byBmYWN0b3Igc28gaXQnbGwgc29ydCBjb3JyZWN0bHkgIApicm9hZGJhbmRfdXNlIDwtIGJyb2FkYmFuZF91c2UgJT4lIAogIG11dGF0ZShQVU1BID0gYXMuY2hhcmFjdGVyKFBVTUEpLAogICAgICAgICBicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUgPSBmYWN0b3IoYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYygiSGlnaCIsICJNZWRpdW0gSGlnaCIsICJNZWRpdW0gTG93IiwgIkxvdyIpKSkKCnB1bWFzXzIwMTBfZ2VvanNvbiA8LSBwdW1hc18yMDEwX2dlb2pzb24gJT4lIAogIGxlZnRfam9pbihicm9hZGJhbmRfdXNlLCBieSA9ICJQVU1BIikKCnB1bWFzXzIwMTBfZ2VvanNvbiAlPiUgcHJpbnQoKQoKCmBgYAoKLSAgIE1hcCB0aGUgYGdlb21fc2ZgIGZpbGwgYWVzdGhldGljIHRvIGJyb2FkYmFuZF9hZG9wdGlvbgoKLSAgIFVzZSBzY2FsZV9maWxsX2dyYWRpZW50IHRvIHNwZWNpZnkgZmlsbCBjb2xvcnMKCmBgYHtyfQoKICBnZ3Bsb3QoKSArIAogIGdlb21fc2YoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgIGFlcyhmaWxsID0gYnJvYWRiYW5kX2Fkb3B0aW9uKSwKICAgIGNvbG9yID0gImdyZXkiCiAgICApICsKICBnZW9tX3BvaW50KGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLAogICAgICAgICAgICAgYWVzKHg9bG9uZ2l0dWRlLAogICAgICAgICAgICAgICAgIHk9bGF0aXR1ZGUsCiAgICAgICAgICAgICAgICAgY29sb3IgPSBvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIHNpemUgPSAyKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKG5hbWUgPSAiU3lzdGVtIix2YWx1ZXMgPSBsaWJyYXJ5X3N5c3RlbV9jb2xvcnMpICsKICBzY2FsZV9maWxsX2dyYWRpZW50KGxvdyA9ICJncmV5NzAiLGhpZ2ggPSAiZ3JleTEwIiwgbGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpLCBuYW1lID0gIkJyb2FkYmFuZCBhZG9wdGlvbiIpICsKICBjb29yZF9zZigpICsKICB0aGVtZV92b2lkKCkKCmBgYAoKLSAgIE9yIGRlZmluZSBkaXNjcmV0ZSBjb2xvcnMgYW5kIHVzZSBgc2NhbGVfZmlsbF9tYW51YWxgCgpgYGB7cn0KCmJyb2FkYmFuZF9jb2xzIDwtIGMoImdyZXkxMCIsImdyZXkzMCIsImdyZXk1MCIsImdyZXk3MCIpICU+JSBzZXRfbmFtZXMobGV2ZWxzKHB1bWFzXzIwMTBfZ2VvanNvbiRicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUpKQoKCmdncGxvdCgpICsgCiAgZ2VvbV9zZihkYXRhID0gcHVtYXNfMjAxMF9nZW9qc29uLAogICAgYWVzKGZpbGwgPSBicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUpLAogICAgY29sb3IgPSAiZ3JleSIKICAgICkgKwogIGdlb21fcG9pbnQoZGF0YSA9IHB1YmxpY19saWJyYXJpZXMsCiAgICAgICAgICAgICBhZXMoeD1sb25naXR1ZGUsCiAgICAgICAgICAgICAgICAgeT1sYXRpdHVkZSwKICAgICAgICAgICAgICAgICBjb2xvciA9IG92ZXJhZ2VuY3kpLAogICAgICAgICAgICAgc2l6ZSA9IDIpICsKICBzY2FsZV9jb2xvcl9tYW51YWwobmFtZSA9ICJTeXN0ZW0iLHZhbHVlcyA9IGxpYnJhcnlfc3lzdGVtX2NvbG9ycykgKwogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGJyb2FkYmFuZF9jb2xzLCBuYW1lID0gIkJyb2FkYmFuZCBhZG9wdGlvbiIpICsKICBjb29yZF9zZigpICsKICB0aGVtZV92b2lkKCkKCgpgYGAKCiMjIyMgTWVkaWFuIGluY29tZQoKPGh0dHBzOi8vd3d3MS5ueWMuZ292L3NpdGUvcGxhbm5pbmcvcGxhbm5pbmctbGV2ZWwvbnljLXBvcHVsYXRpb24vYW1lcmljYW4tY29tbXVuaXR5LXN1cnZleS5wYWdlPgoKLSAgIERvd25sb2FkIGRhdGEgYW5kIGFkZCBpdCB0byBvdXIgcHVtYXNfMjAxMF9nZW9qc29uIGRhdGEgZnJhbWUKCmBgYHtyIGNhY2hlPVRSVUV9CgoKZG93bmxvYWQuZmlsZSh1cmwgPSAiaHR0cHM6Ly93d3cxLm55Yy5nb3YvYXNzZXRzL3BsYW5uaW5nL2Rvd25sb2FkL29mZmljZS9wbGFubmluZy1sZXZlbC9ueWMtcG9wdWxhdGlvbi9hY3MvZWNvbl8yMDE4X2FjczV5cl9wdW1hLnhsc3giLAogICAgICAgICAgICAgIGRlc3RmaWxlID0gImVjb25fMjAxOF9hY3M1eXJfcHVtYS54bHN4IikKCmVjb25kYXRhX3B1bWEgPC0gcmVhZHhsOjpyZWFkX3hsc3goImVjb25fMjAxOF9hY3M1eXJfcHVtYS54bHN4Iiwgc2hlZXQgPSAiRWNvbkRhdGEiKQoKcHVtYXNfMjAxMF9nZW9qc29uIDwtIHB1bWFzXzIwMTBfZ2VvanNvbiAlPiUgCiAgbGVmdF9qb2luKGVjb25kYXRhX3B1bWEgJT4lIAogICAgICAgICAgICAgIHNlbGVjdChQVU1BID0gR2VvSUQsR2VvZ05hbWUsbWVkaWFuX2hvdXNlaG9sZF9pbmNvbWUgPSBNZEhISW5jRSkgJT4lIAogICAgICAgICAgICAgIG11dGF0ZShtZWRpYW5faW5jb21lX2NhdGVnb3J5ID0gCiAgICAgICAgICAgICAgICAgICAgICAgY3V0KG1lZGlhbl9ob3VzZWhvbGRfaW5jb21lLAogICAgICAgICAgICAgICAgICAgICAgICAgICBicmVha3MgPSBjKDAsNTAwMDAsNzUwMDAsMTAwMDAwLDE1MDAwMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIiQwLTUwSyIsIiQ1MC03NUsiLCIkNzUtMTAwSyIsIiQxMDBLKyIpKSkpCgoKYGBgCgotICAgQ3JlYXRlIGEgbmV3IGNvbG9yIHNjYWxlIHdpdGggc29tZSBncmVlbnMKCmBgYHtyfQptZWRpYW5faW5jb21lX2NhdGVnb3J5X2NvbHMgPC0gYygiI2E5Y2NiYyIsIiM3ZmIyOWIiLCIjNGM3ZjY4IiwiIzMzNTU0NSIpICU+JSBzZXRfbmFtZXMobGV2ZWxzKHB1bWFzXzIwMTBfZ2VvanNvbiRtZWRpYW5faW5jb21lX2NhdGVnb3J5KSkKCmdncGxvdCgpICsgCiAgZ2VvbV9zZihkYXRhID0gcHVtYXNfMjAxMF9nZW9qc29uLAogICAgYWVzKGZpbGwgPSBtZWRpYW5faW5jb21lX2NhdGVnb3J5KSwKICAgIGNvbG9yID0gImdyZXkiCiAgICApICsKICBnZW9tX3BvaW50KGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLAogICAgICAgICAgICAgYWVzKHg9bG9uZ2l0dWRlLAogICAgICAgICAgICAgICAgIHk9bGF0aXR1ZGUsCiAgICAgICAgICAgICAgICAgY29sb3IgPSBvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIHNpemUgPSAyKSArCiAgc2NhbGVfY29sb3JfbWFudWFsKG5hbWUgPSAiU3lzdGVtIix2YWx1ZXMgPSBsaWJyYXJ5X3N5c3RlbV9jb2xvcnMpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBtZWRpYW5faW5jb21lX2NhdGVnb3J5X2NvbHMsIG5hbWUgPSAiTWVkaWFuIGluY29tZSIpICsKICBjb29yZF9zZigpICsKICB0aGVtZV92b2lkKCkgCgpgYGAKCiMgTGVhZmxldDogaW50ZXJhY3RpdmUgbWFwcwoKYGBge3J9CmxpYnJhcnkobGVhZmxldCkKCmBgYAoKW0xlYWZsZXRdKGh0dHBzOi8vbGVhZmxldGpzLmNvbSk6IG9wZW4gc291cmNlIGphdmFzY3JpcHQgbGlicmFyeSBmb3IgbWFraW5nIGludGVyYWN0aXZlIG1hcHMgb24gdmFyaW91cyBwbGF0Zm9ybXMKClIgaGFzIGEgYGxlYWZsZXRgIHBhY2thZ2UgdGhhdCBsZXRzIHlvdSBjcmVhdGUgbGVhZmxldCAid2lkZ2V0cyIgd2l0aGluIFIKCkNyZWF0ZSBhIG1hcCwgYWRkIHRpbGVzLCBzZXQgdGhlIHZpZXcKCi0gICBTaW1pbGFyIHRvIGdncGxvdCBpbiB0aGF0IHdlIGtlZXAgYWRkaW5nIGxheWVycy9lbGVtZW50cywgZXhjZXB0IHdlIGNvbnRpbnVlIHVzaW5nIHRoZSBtYWdyaXR0ciBwaXBlIGAlPiVgIHRvIGFkZCBlbGVtZW50cyB0byBhIGxlYWZsZXQgbWFwIChpbnN0ZWFkIG9mIHRoZSBnZ3Bsb3QgYCtgKQoKLSAgIGZvciBub3cgd2UncmUgY3JlYXRpbmcgYSBtYXAgd2l0aG91dCBhbnkgZGF0YQoKYGBge3J9CmxlYWZsZXQoKSAlPiUgCiAgI2FkZFRpbGVzKCkgJT4lIAogIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQogIHNldFZpZXcobGF0ID0gNDAuNyxsbmcgPSAtNzQsIHpvb20gPSAxMCkKCmBgYAoKQWRkIG91ciBwdWJsaWMgbGlicmFyeSBicmFuY2hlcwoKLSAgIGBhZGRDaXJjbGVzKClgIGFkZHMgcG9pbnRzIHRvIHRoZSBtYXAKCi0gICBjYW4gc3BlY2lmeSB0aGUgZGF0YSBzb3VyY2Ugd2l0aGluIHRoZSBgYWRkQ2lyY2xlcygpYCBjYWxsIG9yIGluIHRoZSBvcmlnaW5hbCBgbGVhZmxldCgpYCBjYWxsCgotICAgdG8gcmVmZXIgdG8gZmllbGRzIGluIG91ciBkYXRhIGZyYW1lLCB1c2UgdGhlIHRpbGRlIHByZWZpeCBcfgoKYGBge3J9CgpsZWFmbGV0KCkgJT4lCiAgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgJT4lCiAgc2V0VmlldyhsYXQgPSA0MC43LGxuZyA9IC03NCwgem9vbSA9IDEwKSAlPiUKICBhZGRDaXJjbGVzKGRhdGEgPSBwdWJsaWNfbGlicmFyaWVzLAogICAgICAgICAgICAgbG5nID0gfmxvbmdpdHVkZSwKICAgICAgICAgICAgIGxhdCA9IH5sYXRpdHVkZQogICAgICAgICAgICAgKQoKCmBgYAoKLSAgIFVzZSBgY29sb3JGYWN0b3IoKWAgdG8gY3JlYXRlIGEgcGFsZXR0ZSB0byBjb2xvciB0aGUgY2lyY2xlcyBieSBzeXN0ZW0KCi0gICBVc2UgdGhlIGBsYWJlbGAgYW5kIGBwb3B1cGAgYXJndW1lbnRzIHRvIGFkZCBob3Zlci9jbGljayBpbmZvCgpgYGB7cn0KcGFsX3N5c3RlbSA8LSBjb2xvckZhY3RvcihwYWxldHRlID0gbGlicmFyeV9zeXN0ZW1fY29sb3JzICwKICAgICAgICAgICAgICAgICAgICAgICAgICBkb21haW4gPSB1bmlxdWUocHVibGljX2xpYnJhcmllcyRvdmVyYWdlbmN5KSwKICAgICAgICAgICAgICAgICAgICAgICAgICBvcmRlcmVkID0gVCkKCmxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBzZXRWaWV3KGxhdCA9IDQwLjcsbG5nID0gLTc0LCB6b29tID0gMTApICU+JQogIGFkZENpcmNsZXMoZGF0YSA9IHB1YmxpY19saWJyYXJpZXMsCiAgICAgICAgICAgICBsbmcgPSB+bG9uZ2l0dWRlLAogICAgICAgICAgICAgbGF0ID0gfmxhdGl0dWRlLAogICAgICAgICAgICAgY29sb3IgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIGZpbGwgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICAgICAgICAgIHJhZGl1cyA9IDEwMCwKICAgICAgICAgICAgIGxhYmVsID0gfmZhY25hbWUsCiAgICAgICAgICAgICBwb3B1cCA9IH5hZGRyZXNzCiAgICAgICAgICAgICApICU+JSAKICBhZGRMZWdlbmQocG9zaXRpb24gPSAidG9wbGVmdCIsCiAgICAgICAgICAgIHBhbCA9IHBhbF9zeXN0ZW0sIAogICAgICAgICAgICB2YWx1ZXMgPSB1bmlxdWUocHVibGljX2xpYnJhcmllcyRvdmVyYWdlbmN5KSkKCgoKCmBgYAoKQmVjYXVzZSB3ZSBoYXZlIGEgYmFzZW1hcCwgd2UgZG9uJ3QgbmVlZCB0aGUgUFVNQSBwb2x5Z29ucyB0byBjcmVhdGUgYSBiYXNpYyBtYXAuIEJ1dCB3ZSBjYW4gYWRkIHRoZW0gaWYgd2Ugd2FudCBvdXIgY2hsb3JvcGxldGhzIG9uIHRoZSBpbnRlcmFjdGl2ZSBtYXAuCgotICAgRmlsbCBjb2xvciBhcmd1bWVudCBpbiBsZWFmbGV0IGlzIGBmaWxsQ29sb3I7YCBzcGVjaWZ5IG9wYWNpdHkgd2l0aCBgZmlsbE9wYWNpdHlgCgotICAgVXNlIGBzdHJva2VgIHRvIHNwZWNpZnkgd2hldGhlciBib3VuZGFyeSBsaW5lcyBhcmUgZHJhd247IGBjb2xvcmAgLCBgd2VpZ2h0YCwgYW5kIGBvcGFjaXR5YCB0byBjb250cm9sIHRoZWlyIGFwcGVhcmFuY2UKCmBgYHtyfQoKcGFsX2Jyb2FkYmFuZCA8LSBjb2xvckZhY3RvcihwYWxldHRlID0gYnJvYWRiYW5kX2NvbHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRvbWFpbiA9IGxldmVscyhwdW1hc18yMDEwX2dlb2pzb24kYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBvcmRlcmVkID0gVCkKCgpsZWFmbGV0KCkgJT4lCiAgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgJT4lCiAgc2V0VmlldyhsYXQgPSA0MC43LGxuZyA9IC03NCwgem9vbSA9IDEwKSAlPiUKICBhZGRQb2x5Z29ucyhkYXRhID0gcHVtYXNfMjAxMF9nZW9qc29uLAogICAgICAgICAgICAgIGZpbGxDb2xvciA9IH5wYWxfYnJvYWRiYW5kKGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSksCiAgICAgICAgICAgICAgZmlsbE9wYWNpdHkgPSAuNywKICAgICAgICAgICAgICBjb2xvciA9ICJncmV5IiwKICAgICAgICAgICAgICBzdHJva2UgPSBULAogICAgICAgICAgICAgIHdlaWdodCA9IDEsCiAgICAgICAgICAgICAgbGFiZWwgPSB+UFVNQSwKICAgICAgICAgICAgICBwb3B1cCA9IH5wYXN0ZTAoIjxiPlBVTUEgIixQVU1BLCI8L2I+IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPiIsIEdlb2dOYW1lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+QnJvYWRiYW5kIGFkb3B0aW9uOiAiLHJvdW5kKGJyb2FkYmFuZF9hZG9wdGlvbioxMDAsMCksIiUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+QnJvYWRiYW5kIGFkb3B0aW9uIGNhdGVnb3J5OiAiLGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSkpIAoKCmBgYAoKQ29tYmluZSBpdCB3aXRoIHRoZSBsaWJyYXJpZXMgYW5kIGFkZCBhIGxlZ2VuZAoKLSAgIE9yZGVyIG1hdHRlcnMgLSBlYWNoIHN1Y2Nlc3NpdmUgbGF5ZXIgaXMgcGxvdHRlZCBvbiB0b3Agb2YgdGhlIHByZXZpb3VzIG9uZXMKCmBgYHtyfQoKbGVhZmxldCgpICU+JQogIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQogIHNldFZpZXcobGF0ID0gNDAuNyxsbmcgPSAtNzQsIHpvb20gPSAxMCkgJT4lCiAgYWRkUG9seWdvbnMoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgICAgICAgICAgICBmaWxsQ29sb3IgPSB+cGFsX2Jyb2FkYmFuZChicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUpLAogICAgICAgICAgICAgIGZpbGxPcGFjaXR5ID0gLjcsCiAgICAgICAgICAgICAgY29sb3IgPSAiZ3JleSIsCiAgICAgICAgICAgICAgc3Ryb2tlID0gVCwKICAgICAgICAgICAgICB3ZWlnaHQgPSAxLAogICAgICAgICAgICAgIGxhYmVsID0gflBVTUEsCiAgICAgICAgICAgICAgcG9wdXAgPSB+cGFzdGUwKCI8Yj5QVU1BICIsUFVNQSwiPC9iPiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+IiwgR2VvZ05hbWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj5Ccm9hZGJhbmQgYWRvcHRpb246ICIscm91bmQoYnJvYWRiYW5kX2Fkb3B0aW9uKjEwMCwwKSwiJSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj5Ccm9hZGJhbmQgYWRvcHRpb24gY2F0ZWdvcnk6ICIsYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSkgJT4lIAogICAgYWRkQ2lyY2xlcyhkYXRhID0gcHVibGljX2xpYnJhcmllcywKICAgICAgICAgICAgIGxuZyA9IH5sb25naXR1ZGUsCiAgICAgICAgICAgICBsYXQgPSB+bGF0aXR1ZGUsCiAgICAgICAgICAgICBjb2xvciA9IH5wYWxfc3lzdGVtKG92ZXJhZ2VuY3kpLAogICAgICAgICAgICAgZmlsbCA9IH5wYWxfc3lzdGVtKG92ZXJhZ2VuY3kpLAogICAgICAgICAgICAgcmFkaXVzID0gMTAwLAogICAgICAgICAgICAgbGFiZWwgPSB+ZmFjbmFtZSwKICAgICAgICAgICAgIHBvcHVwID0gfmFkZHJlc3MKICAgICAgICAgICAgICkgJT4lIAogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJ0b3BsZWZ0IiwKICAgICAgICAgICAgcGFsID0gcGFsX3N5c3RlbSwgCiAgICAgICAgICAgIHZhbHVlcyA9IHB1YmxpY19saWJyYXJpZXMkb3ZlcmFnZW5jeSwKICAgICAgICAgICAgdGl0bGUgPSAiTGlicmFyeSBTeXN0ZW0iKSAlPiUgCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gInRvcGxlZnQiLAogICAgICAgICAgICBwYWwgPSBwYWxfYnJvYWRiYW5kLCAKICAgICAgICAgICAgdmFsdWVzID0gIHB1bWFzXzIwMTBfZ2VvanNvbiRicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUsCiAgICAgICAgICAgIG9wYWNpdHkgPSAuNywKICAgICAgICAgICAgdGl0bGUgPSAiQnJvYWRiYW5kIEFjY2VzcyIpCgpgYGAKClZpc3VhbGl6ZSBtb3JlIHRoYW4gb25lIGxheWVyCgotICAgVXNlIGBncm91cGAgYXJndW1lbnQgd2l0aGluIGVhY2ggbGF5ZXIgdG8gc3BlY2lmeSB3aGljaCBncm91cCBpdCBiZWxvbmdzIHRvCgotICAgYGFkZExheWVyc0NvbnRyb2xgIGFkZHMgYSBjb250cm9sIHRvIHRvZ2dsZSBiZXR3ZWVuIGxheWVycyBvciB0dXJuIGxheWVycyBvbiBhbmQgb2ZmCgotICAgYXNzaWduaW5nIHRoZSBtYXAgdG8gYW4gb2JqZWN0IGNyZWF0ZXMgYSBsZWFmbGV0IG9iamVjdCB3aXRoIGNsYXNzIGBodG1sd2lkZ2V0YAoKYGBge3J9CnBhbF9pbmNvbWUgPC0gY29sb3JGYWN0b3IocGFsZXR0ZSA9IG1lZGlhbl9pbmNvbWVfY2F0ZWdvcnlfY29scywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZG9tYWluID0gbGV2ZWxzKHB1bWFzXzIwMTBfZ2VvanNvbiRtZWRpYW5faW5jb21lX2NhdGVnb3J5KSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBvcmRlcmVkID0gVCkKCmxpYnJhcmllc19tYXAgPC0gbGVhZmxldCgpICU+JQogIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQogIHNldFZpZXcobGF0ID0gNDAuNyxsbmcgPSAtNzQsIHpvb20gPSAxMCkgJT4lCiAgYWRkUG9seWdvbnMoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgICAgICAgICAgICBmaWxsQ29sb3IgPSB+cGFsX2Jyb2FkYmFuZChicm9hZGJhbmRfYWRvcHRpb25fcXVhcnRpbGUpLAogICAgICAgICAgICAgIGZpbGxPcGFjaXR5ID0gLjcsCiAgICAgICAgICAgICAgY29sb3IgPSAiZ3JleSIsCiAgICAgICAgICAgICAgc3Ryb2tlID0gVCwKICAgICAgICAgICAgICB3ZWlnaHQgPSAxLAogICAgICAgICAgICAgIGxhYmVsID0gflBVTUEsCiAgICAgICAgICAgICAgcG9wdXAgPSB+cGFzdGUwKCI8Yj5QVU1BICIsUFVNQSwiPC9iPiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+IiwgR2VvZ05hbWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj5Ccm9hZGJhbmQgYWRvcHRpb246ICIscm91bmQoYnJvYWRiYW5kX2Fkb3B0aW9uKjEwMCwwKSwiJSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIjxicj5Ccm9hZGJhbmQgYWRvcHRpb24gY2F0ZWdvcnk6ICIsYnJvYWRiYW5kX2Fkb3B0aW9uX3F1YXJ0aWxlKSwKICAgICAgICAgICAgICBncm91cCA9ICJCcm9hZGJhbmQiKSAlPiUgCiAgYWRkUG9seWdvbnMoZGF0YSA9IHB1bWFzXzIwMTBfZ2VvanNvbiwKICAgICAgICAgICAgICBmaWxsQ29sb3IgPSB+cGFsX2luY29tZShtZWRpYW5faW5jb21lX2NhdGVnb3J5KSwKICAgICAgICAgICAgICBmaWxsT3BhY2l0eSA9IC43LAogICAgICAgICAgICAgIGNvbG9yID0gImdyZXkiLAogICAgICAgICAgICAgIHN0cm9rZSA9IFQsCiAgICAgICAgICAgICAgd2VpZ2h0ID0gMSwKICAgICAgICAgICAgICBsYWJlbCA9IH5QVU1BLAogICAgICAgICAgICAgIHBvcHVwID0gfnBhc3RlMCgiPGI+UFVNQSAiLFBVTUEsIjwvYj4iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPiIsIEdlb2dOYW1lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICI8YnI+TWVkaWFuIGluY29tZTogJCIsZm9ybWF0KG1lZGlhbl9ob3VzZWhvbGRfaW5jb21lLGJpZy5tYXJrID0gIiwiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGJyPk1lZGlhbiBpbmNvbWUgY2F0ZWdvcnk6ICIsbWVkaWFuX2luY29tZV9jYXRlZ29yeSksCiAgICAgICAgICAgICAgZ3JvdXAgPSAiaW5jb21lIikgJT4lIAogICAgYWRkQ2lyY2xlcyhkYXRhID0gcHVibGljX2xpYnJhcmllcywKICAgICAgICAgICAgIGxuZyA9IH5sb25naXR1ZGUsCiAgICAgICAgICAgICBsYXQgPSB+bGF0aXR1ZGUsCiAgICAgICAgICAgICBjb2xvciA9IH5wYWxfc3lzdGVtKG92ZXJhZ2VuY3kpLAogICAgICAgICAgICAgZmlsbCA9IH5wYWxfc3lzdGVtKG92ZXJhZ2VuY3kpLAogICAgICAgICAgICAgb3BhY2l0eSA9IDEsCiAgICAgICAgICAgICBmaWxsT3BhY2l0eSA9IDEsCiAgICAgICAgICAgICByYWRpdXMgPSAxMDAsCiAgICAgICAgICAgICBsYWJlbCA9IH5mYWNuYW1lLAogICAgICAgICAgICAgcG9wdXAgPSB+YWRkcmVzcyMsCiAgICAgICAgICAgICAjZ3JvdXAgPSAiQnJvYWRiYW5kIgogICAgICAgICAgICAgKSAlPiUgCiAgICAjIGFkZENpcmNsZXMoZGF0YSA9IHB1YmxpY19saWJyYXJpZXMsCiAgICAjICAgICAgICAgIGxuZyA9IH5sb25naXR1ZGUsCiAgICAjICAgICAgICAgIGxhdCA9IH5sYXRpdHVkZSwKICAgICMgICAgICAgICAgY29sb3IgPSB+cGFsX3N5c3RlbShvdmVyYWdlbmN5KSwKICAgICMgICAgICAgICAgZmlsbCA9IH5wYWxfc3lzdGVtKG92ZXJhZ2VuY3kpLAogICAgIyAgICAgICAgICBvcGFjaXR5ID0gMSwKICAgICMgICAgICAgICAgZmlsbE9wYWNpdHkgPSAxLAogICAgIyAgICAgICAgICByYWRpdXMgPSAxMDAsCiAgICAjICAgICAgICAgIGxhYmVsID0gfmZhY25hbWUsCiAgICAjICAgICAgICAgIHBvcHVwID0gfmFkZHJlc3MsCiAgICAjICAgICAgICAgIGdyb3VwID0gIkluY29tZSIKICAgICMgICAgICAgICAgKSAlPiUgCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gInRvcGxlZnQiLAogICAgICAgICAgICBwYWwgPSBwYWxfc3lzdGVtLCAKICAgICAgICAgICAgdmFsdWVzID0gcHVibGljX2xpYnJhcmllcyRvdmVyYWdlbmN5LAogICAgICAgICAgICB0aXRsZSA9ICJMaWJyYXJ5IFN5c3RlbSIpICU+JSAKICBhZGRMZWdlbmQocG9zaXRpb24gPSAidG9wbGVmdCIsCiAgICAgICAgICAgIHBhbCA9IHBhbF9icm9hZGJhbmQsIAogICAgICAgICAgICB2YWx1ZXMgPSAgcHVtYXNfMjAxMF9nZW9qc29uJGJyb2FkYmFuZF9hZG9wdGlvbl9xdWFydGlsZSwKICAgICAgICAgICAgb3BhY2l0eSA9IC43LAogICAgICAgICAgICB0aXRsZSA9ICJCcm9hZGJhbmQgQWNjZXNzIiwKICAgICAgICAgICAgZ3JvdXAgPSAiQnJvYWRiYW5kIikgJT4lIAogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJ0b3BsZWZ0IiwKICAgICAgICAgICAgcGFsID0gcGFsX2luY29tZSwgCiAgICAgICAgICAgIHZhbHVlcyA9ICBwdW1hc18yMDEwX2dlb2pzb24kbWVkaWFuX2luY29tZV9jYXRlZ29yeSwKICAgICAgICAgICAgb3BhY2l0eSA9IC43LAogICAgICAgICAgICB0aXRsZSA9ICJNZWRpYW4gaG91c2Vob2xkIGluY29tZSIsCiAgICAgICAgICAgIGdyb3VwID0gIkluY29tZSIpICU+JSAKICBhZGRMYXllcnNDb250cm9sKAogICAgICAgICAgICBiYXNlR3JvdXBzID0gYygiQnJvYWRiYW5kIiwiSW5jb21lIiksCiAgICAgICAgICAgIG9wdGlvbnMgPSBsYXllcnNDb250cm9sT3B0aW9ucyhjb2xsYXBzZWQgPSBGKSMsCiAgICAgICAgICAgICNvdmVybGF5R3JvdXBzID0gYygiQnJhbmNoZXMiKQogICkgCgoKbGlicmFyaWVzX21hcAoKYGBgCgpUaGUgaHRtbHdpZGdldHMgcGFja2FnZSBsZXRzIHlvdSBzYXZlIHRoaXMgbWFwIGFzIGFuIGh0bWwgZmlsZSwgd2hpY2ggY2FuIHRoZW4gYmUgb3BlbmVkIGluIGEgYnJvd3NlciBvciBlbWJlZGRlZCBpbiBhIHdlYiBwYWdlCgpgYGB7cn0KaHRtbHdpZGdldHM6OnNhdmVXaWRnZXQobGlicmFyaWVzX21hcCwKICAgICAgICAgICAgICAgICAgICAgICAgZmlsZSA9ICJsaWJyYXJpZXNfbWFwLmh0bWwiLAogICAgICAgICAgICAgICAgICAgICAgICB0aXRsZSA9ICJOWUMgTGlicmFyaWVzOiAgQnJvYWRiYW5kIEFjY2VzcyBhbmQgTWVkaWFuIEluY29tZSIpCgpgYGAKCiMjIE90aGVyIHJlc291cmNlcwoKLSAgIFtgdGlkeWNlbnN1c2BdKGh0dHBzOi8vd2Fsa2VyLWRhdGEuY29tL3RpZHljZW5zdXMvKSBwYWNrYWdlOiBhbWF6aW5nIFIgaW50ZXJmYWNlIGZvciBDZW5zdXMvQUNTIEFQSSwgaW5jbHVkaW5nIGdlb21ldHJpZXMKCiAgICAtICAgW0FuYWx5emluZyBDZW5zdXMgRGF0YTogTWV0aG9kcyBNYXBzIGFuZCBNb2RlbHMgaW4gUl0oaHR0cHM6Ly93YWxrZXItZGF0YS5jb20vY2Vuc3VzLXIvKQoKLSAgIFtgdGlncmlzYF0oaHR0cHM6Ly9naXRodWIuY29tL3dhbGtlcmtlL3RpZ3JpcykgcGFja2FnZTogbW9yZSBDZW5zdXMgZ2VvbWV0cmllcwoKLSAgIFtgcm5hdHVyYWxlYXJ0aGBdKGh0dHBzOi8vZG9jcy5yb3BlbnNjaS5vcmcvcm5hdHVyYWxlYXJ0aC8pOiB3b3JsZCBtYXAgZGF0YQoKLSAgIFtgdG1hcGBdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy90bWFwL3ZpZ25ldHRlcy90bWFwLWdldHN0YXJ0ZWQuaHRtbCk6IEFsdGVybmF0aXZlIHBhY2thZ2UgZm9yIHRtYXBwaW5nIGluIFIsIHdpdGggZ2dwbG90LWxpa2Ugc3ludGF4IC0gc29tZSBmaW5kIHRoaXMgdGhlIGJlc3Qgd2F5IHRvIGdldCBzdGFydGVkCgotICAgW2BtYXBib3hhcGlgXShodHRwczovL3dhbGtlci1kYXRhLmNvbS9tYXBib3hhcGkvKSBwYWNrYWdlOiBtb3JlIGJhc2VtYXAgb3B0aW9ucywgZ2VvY29kaW5nIHNlcnZpY2VzLCBpc29jaHJvbmVzIChyZXF1aXJlcyBzZXR0aW5nIHVwIGEgbWFwYm94IGFjY291bnQpCgotICAgR2VvY29kaW5nCgogICAgLSAgIFdpdGhpbiBOWUMsIFtHZW9zdXBwb3J0XShodHRwczovL3d3dzEubnljLmdvdi9zaXRlL3BsYW5uaW5nL2RhdGEtbWFwcy9vcGVuLWRhdGEvZHduLWdkZS1ob21lLnBhZ2UpCgogICAgLSAgIFtVcmJhbiBJbnN0aXR1dGUgb3ZlcnZpZXcgb2Ygb3RoZXIgdG9vbHNdKGh0dHBzOi8vdXJiYW4taW5zdGl0dXRlLm1lZGl1bS5jb20vY2hvb3NpbmctYS1nZW9jb2Rlci1mb3ItdGhlLXVyYmFuLWluc3RpdHV0ZS04NjE5MmY2NTZjNWYpCgotICAgW0dlb2NvbXB1dGF0aW9uIHdpdGggUl0oaHR0cHM6Ly9nZW9jb21wci5yb2JpbmxvdmVsYWNlLm5ldC9pbmRleC5odG1sKQo=